package com.masonluo.mlonlinejudge.service;


import com.fasterxml.jackson.databind.ObjectMapper;
import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.Session;
import com.masonluo.mlonlinejudge.api.TestcaseService;
import com.masonluo.mlonlinejudge.exceptions.ServerProcessException;
import com.masonluo.mlonlinejudge.model.Pair;
import com.masonluo.mlonlinejudge.model.TestCase;
import com.masonluo.mlonlinejudge.model.TestCaseInfo;
import com.masonluo.mlonlinejudge.model.dto.TestCaseUploadDto;
import com.masonluo.mlonlinejudge.util.SFTP;
import com.masonluo.mlonlinejudge.utils.*;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.zip.Zip64Mode;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.io.FileUtils;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.json.GsonBuilderUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.net.*;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import net.lingala.zip4j.core.ZipFile;
import net.lingala.zip4j.exception.ZipException;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import static com.masonluo.mlonlinejudge.util.SFTP.disConn;
import static com.masonluo.mlonlinejudge.util.SFTP.getConnect;


@Service
public class TestcaseServiceImpl implements TestcaseService {

    // TODO 修改测试文件的存放路径
    // private final String testCaseDir = "/home/OnlineJudgeDeploy/data/backend/test_case";// /home/OnlineJudgeDeploy/data/backend/test_case
    private final String testCaseDir = "/usr/local/mloj/OnlineJudgeDeploy/data/backend/test_case";// /home/OnlineJudgeDeploy/data/backend/test_case
    // TODO 修改临时文件的存放路径
    // private final String tmpDir = "/home/mlojDir/mlojHome/tmp";
    private final String tmpDir = "/usr/local/mloj/tmp";
//    private final String tmpDir = "D:/home/home";
//    private final String testCaseDir = "E:\\OJ_FILE\\test_case";
//    private final String tmpDir = "E:\\OJ_FILE\\tmp";
    private final ObjectMapper objectMapper = new ObjectMapper();

    private static final int IN_MASK = 1;

    private static final int OUT_MASK = 1 << 1;

    private static final int RES = IN_MASK | OUT_MASK;

    private static final int _1MB = 1024 * 1024;
    private List<Pair<String,String>> answerList;
    private List<Pair<String,String>> inputContents;

    @Override
    public TestCaseUploadDto uploadTestCase(MultipartFile zipFile) throws IOException{
        try {
            answerList = new ArrayList<>();
            inputContents = new ArrayList<>();
            TestCaseUploadDto uploadDto;
            // 生成测试数据id，作为测试数据的文件夹名称
            String testCaseId = UUIDUtils.getUuid();
            System.out.println("测试数据文件夹名称"+testCaseId);
            // 将文件上传到临时目录中
            // 生成父目录
            File parentDir = new File(tmpDir + "/" + testCaseId);
            if (!parentDir.mkdirs()) {
                throw new ServerProcessException("Can't create a new directory");
            }
            String filename = zipFile.getOriginalFilename();
            System.out.println("filename+++++++++++++++"+filename);
            if (StringUtils.isBlank(filename)) {
                filename = "target.zip";
            }
            File target = new File(parentDir, filename);
            String targetPath = target.getAbsolutePath();
            System.out.println("targetPath+++++"+targetPath);
            zipFile.transferTo(target);
            System.out.println("移动zip文件对象到指定文件夹+++++++++++++++成功");
            // 进行解压到当前目录上
            //ZipUtils.unzip(targetPath);有bug ,以下方法对解压功能进行了重写
            ZipFile zipFile1 = new ZipFile(target);
            zipFile1.extractAll(tmpDir+"/"+testCaseId);
            System.out.println("解压文件夹到当前目录+++++++++++++++成功");
            System.out.println("要删除的文件是:"+target.getName());
            if (!target.delete()) {
                throw new ServerProcessException("Can't delete file in the path, does not has the right");
            }
            System.out.println("删除原压缩文件+++++++++++++++成功");
            // 验证上传文件的格式，判断是否是正确的命名
            verifyTestCasePattern(parentDir);
            System.out.println("文件格式校验+++++++++++++++成功");
            // 生成info实体
            TestCaseInfo info = generateTestCaseInfo(parentDir);
            System.out.println("info实体生成++++++++++++++成功");
            // 保存info文件
            generateInfoFile(info, parentDir);
            System.out.println("保存info文件到in out同级的目录++++++++++++成功");
            // 将该目录移动到test_case的目录下面
            moveDirectory(parentDir, testCaseDir);
            System.out.println("移动当前测试样例目录到OJ评判机的指定目录++++++++++++成功");
            // 生成即将要返回的信息
            uploadDto = generateTestCaseUploadDto(testCaseId, info);
            uploadDto.setAnswers(answerList);
            uploadDto.setInputContents(inputContents);
            answerList = null;
            inputContents=null;
            System.out.println("生成测试样例对象+++++++++++++++++成功");
            return uploadDto;
        } catch (IOException | ZipException e) {
            throw new ServerProcessException("Can't save the zip file in disk", e);
        }
    }


    private TestCaseUploadDto generateTestCaseUploadDto(String testCaseId, TestCaseInfo info) {
        TestCaseUploadDto uploadDto = new TestCaseUploadDto();
        uploadDto.setTestCaseId(testCaseId);
        List<Pair<String, String>> testCaseFiles = new ArrayList<>();
        for (Integer idx : info.getTestCase().keySet()) {
            String in = String.format("%d.in", idx);
            String out = String.format("%d.out", idx);
            testCaseFiles.add(new Pair<>(in, out));
        }
        uploadDto.setFiles(testCaseFiles);
        return uploadDto;
    }

    /**
     * 移动文件夹到指定的路径
     */
    private void moveDirectory(File parentDir, String testCaseDir) {
        try {

//  此方法在挂载文件时，移动文件会报错Files.move(parentDir.toPath(), Paths.get(testCaseDir + "/" + parentDir.getName()));
            File file = new File(testCaseDir + "/" + parentDir.getName());
            FileUtils.moveDirectory(parentDir,file);//先复制后删除
            FileUtils.deleteDirectory(parentDir);
        } catch (IOException e) {
            e.printStackTrace();
            throw new ServerProcessException(
                    "Can't move the directory[" +
                            parentDir.getAbsolutePath() + "] to the target path [" + testCaseDir + "]", e);
        }
    }

    /**
     * 将info文件保存到文件夹中
     */
    private void generateInfoFile(TestCaseInfo info, File parentDir) {
        try {
            String infoStr = objectMapper.writeValueAsString(info);
            FileWriter fos = new FileWriter(new File(parentDir, "info"));
            fos.write(infoStr);
            fos.close();
        } catch (IOException e) {
            throw new ServerProcessException("Can't save the info file", e);
        }
    }

    /**
     * 生成info文件实体
     */
    private TestCaseInfo generateTestCaseInfo(File parentDir) {
        TestCaseInfo info = new TestCaseInfo();
        // 目前只提供了非特判
        info.setSpj(false);
        File[] children = parentDir.listFiles();
        if (ObjectUtils.isEmpty(children)) {
            return info;
        }
        Map<Integer, TestCase> testCaseMap = info.getTestCase();
        for (File child : children) {
            String name = child.getName();
            int idx = Integer.parseInt(name.substring(0, name.lastIndexOf(".")));
            String suffix = name.substring(name.lastIndexOf(".") + 1);
            TestCase testCase = null;
            if ((testCase = testCaseMap.get(idx)) == null) {
                testCase = new TestCase();
            }
            if (ObjectUtils.nullSafeEquals(suffix, "in")) {
                testCase.setInputName(name);
                testCase.setInputSize(child.length());
                saveInputContents(child,idx);
            } else {
                testCase.setOutputName(name);
                testCase.setOutputSize(child.length());
                testCase.setStrippedOutputMd5(strippedMd5(child,idx));
            }
            testCaseMap.put(idx, testCase);
        }
        return info;
    }

    /**
     * 将文件独取出来，并且用md5生成摘要
     */
    private String strippedMd5(File child) {
        try {
            StringBuilder sb = new StringBuilder();
            BufferedReader br = new BufferedReader(new FileReader(child));
            String line = null;
            while ((line = br.readLine()) != null) {
                sb.append(line);
                sb.append("\n");
            }

            String content = sb.toString();
            content = sb.substring(0,sb.lastIndexOf("\n"));
//            content = doStripped(content);
            br.close();
            String md5Result = CodecUtils.md5DigestAsHex(content);
            System.out.println("result="+md5Result+"---------");
            return md5Result;
        } catch (IOException e) {
            throw new ServerProcessException("The file [" + child.getName() + "] is not a readable file");
        }
    }
    /**
     * 将文件独取出来，并且用md5生成摘要,同时保存输出文件的内容
     * testCase 表示第几个测试用例
     */
    private String strippedMd5(File child,int testCase) {
        try {
            StringBuilder sb = new StringBuilder();
            BufferedReader br = new BufferedReader(new FileReader(child));
            String line = null;
            while ((line = br.readLine()) != null) {
                sb.append(line);
                sb.append("\n");
            }

            String content = sb.toString();
            content = sb.substring(0,sb.lastIndexOf("\n"));
//            content = doStripped(content);
            br.close();
            if(answerList==null){
                answerList = new ArrayList<>();
            }
            Pair<String,String> pair = new Pair<>();
            pair.setFirst(""+testCase);
            pair.setSecond(content);
            answerList.add(pair);
            String md5Result = CodecUtils.md5DigestAsHex(content);
            System.out.println("result="+md5Result+"---------");
            return md5Result;
        } catch (IOException e) {
            throw new ServerProcessException("The file [" + child.getName() + "] is not a readable file");
        }
    }
    /**
     * 将输入文件的内容读出来，输入文件内容可能为空
     * testCase 表示第几个测试用例
     * */
    private void saveInputContents(File child,int testCase) {
        try {
            StringBuilder sb = new StringBuilder();
            BufferedReader br = new BufferedReader(new FileReader(child));
            String line = null;
            boolean hasContent = false;
            do{
                line = br.readLine();
                if(line!=null){//判断文件是否有内容
                    hasContent=true;
                    sb.append(line);
                    sb.append("\n");
                }
            }while (line!=null);
            String content="";
            if(hasContent)
                content = sb.substring(0,sb.lastIndexOf("\n"));
//            content = doStripped(content);
            br.close();
            if(inputContents==null){
                inputContents = new ArrayList<>();
            }
            Pair<String,String> pair = new Pair<>();
            pair.setFirst(""+testCase);
            pair.setSecond(content);
            inputContents.add(pair);
        } catch (IOException e) {
            throw new ServerProcessException("The file [" + child.getName() + "] is not a readable file");
        }
    }

    /**
     * 将所有的空白字符去除
     */
    private String doStripped(String content) {
        if (content == null) {
            return null;
        }
        String res;
        Pattern pattern = Pattern.compile("\\s*|\t|\r|\n");
        Matcher matcher = pattern.matcher(content);
        res = matcher.replaceAll("");
        return res;
    }

    /**
     * 验证压缩包内的文件是否符合格式
     */
    private void verifyTestCasePattern(File parentDir) {
        File[] children = parentDir.listFiles();
        if (ObjectUtils.isEmpty(children)) {
            throw new IllegalArgumentException("Test case should not be empty, please upload at lease a test case");
        }
        if (children.length % 2 != 0) {
            throw new IllegalArgumentException("Each xx.in file should has another xx.out to verify");
        }

        int[] assistArray = new int[children.length / 2];
        for (File child : children) {
            String name = child.getName();
            if (!child.isFile()) {
                throw new IllegalArgumentException("Test case should not be contain a directory");
            }
            if (!verifyFilename(name)) {
                throw new IllegalArgumentException("The test case file name should be match the pattern '[0-9]+\\.(in|out)'");
            }
            // 分割序号和后缀
            int idx = Integer.parseInt(name.substring(0, name.lastIndexOf(".")));
            String suffix = name.substring(name.lastIndexOf(".") + 1);
            if (idx > assistArray.length) {
                throw new IllegalArgumentException("The test case file should be in order");
            }
            if (ObjectUtils.nullSafeEquals(suffix, "in")) {
                assistArray[idx - 1] |= IN_MASK;
            } else {
                assistArray[idx - 1] |= OUT_MASK;
            }
        }
        for (int i : assistArray) {
            if (i != RES) {
                throw new IllegalArgumentException("in and out file should be appear together\n" + changeToStringName(assistArray));
            }
        }
    }

    private boolean verifyFilename(String name) {
        String pattern = "[0-9]+\\.(in|out)";
        return name.matches(pattern);
    }

    private String changeToStringName(int[] assistArray) {
        StringBuilder sb = new StringBuilder();
        sb.append("\tin\tout\n");
        for (int i = 0; i < assistArray.length; i++) {
            sb.append(i + 1);
            sb.append((assistArray[i] & IN_MASK) == 0 ? 0 : 1);
            sb.append("\t");
            sb.append((assistArray[i] & OUT_MASK) == 0 ? 0 : 1);
        }
        return sb.toString();
    }

    /**
     * 以下为打包下载全部测试用例
     *
     * @param testCaseId
     * @param request
     * @param response
     * @throws Exception
     */
    public void downloadAllAttachment(String testCaseId, HttpServletRequest request, HttpServletResponse response) throws Exception {
        String filePath = testCaseDir + "/" + testCaseId;
//        String filePath = "E:/" + testCaseId;
        File dirFile = new File(filePath);
        ArrayList<String> allFilePath = Dir(dirFile);
        List<File> filesList = new ArrayList<>();
        File[] files = new File[allFilePath.size()];
        String path;
        for (int j = 0; j < allFilePath.size(); j++) {
            //获取目录下的文件路径
            path = allFilePath.get(j);
            File file = new File(path);
            files[j] = file;
            filesList.add(file);
        }
        System.out.println("文件列表：" + filesList + "++路径：" + filePath);
        fileToZip(filesList,tmpDir);
    }

    @Override
    public void deleteTestCase(String testCaseId) throws Exception {
        // TODO 修改测试文件的存放路径
        // String directory = "/home/OnlineJudgeDeploy/data/backend/test_case/" + testCaseId;
        String directory = "/usr/local/mloj/OnlineJudgeDeploy/data/backend/test_case/" + testCaseId;
        SFTP s=new SFTP();
        getConnect(s);//建立连接
        Session session = s.getSession();
        Channel channel = s.getChannel();
        ChannelSftp sftp = s.getSftp();// sftp操作类
        Vector<ChannelSftp.LsEntry> directoryEntries = sftp.ls(directory);
        try {
            for (ChannelSftp.LsEntry file : directoryEntries) {
                if(!String.format(file.getFilename()).equals(".") && !String.format(file.getFilename()).equals("..") ){
                    sftp.cd(directory);
                    sftp.rm(file.getFilename());
                }
            }
            sftp.rmdir(directory);
        } catch (Exception e) {
            throw new Exception(e.getMessage(),e);
        } finally {
            disConn(session,channel,sftp);
        }
    }

    /**
     * 获取文件夹下的所有文件的路径
     */
    public static ArrayList<String> Dir(File dirFile) throws Exception {
        ArrayList<String> dirStrArr = new ArrayList<String>();
        if (dirFile.exists()) {
            //直接取出利用listFiles()把当前路径下的所有文件夹、文件存放到一个文件数组
            File files[] = dirFile.listFiles();
            for (File file : files) {
                //如果传递过来的参数dirFile是以文件分隔符，也就是/或者\结尾，则如此构造
                if (dirFile.getPath().endsWith(File.separator)) {
                    dirStrArr.add(dirFile.getPath() + file.getName());
                } else {
                    //否则，如果没有文件分隔符，则补上一个文件分隔符，再加上文件名，才是路径
                    dirStrArr.add(dirFile.getPath() + File.separator + file.getName());
                }
            }
        }
        return dirStrArr;
    }

    /**
     * 将源文件打包成test.zip文件，并存放到临时目录tmpDir下
     * @param files :待压缩的文件
     * @param tmpDir :压缩后存放路径
     * @return
     */
    public static  boolean fileToZip(List<File> files,String tmpDir) {
        //if (deleteAllFile(tmpDir)){
            //System.out.println("删除历史测试用例成功");
       // }
        boolean flag = false;
        FileInputStream fis = null;
        BufferedInputStream bis = null;
        FileOutputStream fos = null;
        ZipOutputStream zos = null;
        try {
            File zipFile = new File(tmpDir + "/" + "test" + ".zip");//压缩后文件的名称
            if (zipFile.exists()) {
                System.out.println(tmpDir + "目录下存在名字为:" + "test" + ".zip" + "的打包文件.");
            }
            else {
                if(!zipFile.exists()){
                    zipFile.getParentFile().mkdir();
                }
                fos = new FileOutputStream(zipFile);
                zos = new ZipOutputStream(new BufferedOutputStream(fos));
                byte[] bufs = new byte[1024 * 1024];
                for (int i = 0; i < files.size(); i++) {
                    try {
                        //创建ZIP实体，并添加进压缩包
                        ZipEntry zipEntry = new ZipEntry(String.valueOf(files.get(i)));
                        zos.putNextEntry(zipEntry);
                        // 读取待压缩的文件并写进压缩包里
                        fis = new FileInputStream(files.get(i));
                        bis = new BufferedInputStream(fis, 1024 * 1024);
                        int read = 0;
                        while ((read = bis.read(bufs, 0, 1024 * 1024)) != -1) {
                            zos.write(bufs, 0, read);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }

                }
                flag = true;
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        } finally {
            // 关闭流
            try {
                if (null != bis)
                    bis.close();
                if (null != zos)
                    zos.close();
            } catch (IOException e) {
                e.printStackTrace();
                throw new RuntimeException(e);
            }
        }
        return flag;
    }

    /**
     * 删除指定文件夹下的test.zip
     * @param filepath 文件夹路径
     * @return 删除成功返回true,失败返回false
     */
    public  boolean deleteAllFile(String filepath) {
        boolean flag = false;
        File file = new File(filepath);
        if (file.exists()&&file.isFile()){
            file.delete();
            flag=true;
        }
        return flag;
    }
}
